/* 
 * File:   MtpSync.cpp
 * Author: arun
 * 
 * Created on November 21, 2009, 6:02 PM
 */

#include "YBMtpSync.h"
#include <string>
#include <fileref.h>
#include <tag.h>
#include <vector>
#include <sys/stat.h>

extern "C" //All C headers goes in here
{
#include <string.h>
#include "YBpathutils.h"
#include "libgen.h"
}

using namespace std;

YBMtpSync::YBMtpSync() {
}

YBMtpSync::YBMtpSync(const YBMtpSync& orig) {
}

YBMtpSync::~YBMtpSync() {
}

bool YBMtpSync::connect() {
    LIBMTP_mtpdevice_t *device_list, *iter;

    LIBMTP_Init();

    switch (LIBMTP_Get_Connected_Devices(&device_list)) {
        case LIBMTP_ERROR_NO_DEVICE_ATTACHED:
            fprintf(stdout, "mtp-tracks: No Devices have been found\n");
            return false;
        case LIBMTP_ERROR_CONNECTING:
            fprintf(stderr, "mtp-tracks: There has been an error connecting. Exit\n");
            return false;
        case LIBMTP_ERROR_MEMORY_ALLOCATION:
            fprintf(stderr, "mtp-tracks: Memory Allocation Error. Exit\n");
            return false;

            /* Unknown general errors - This should never execute */
        case LIBMTP_ERROR_GENERAL:
        default:
            fprintf(stderr, "mtp-tracks: Unknown error, please report "
                    "this to the libmtp developers\n");
            return false;

            /* Successfully connected at least one device, so continue */
        case LIBMTP_ERROR_NONE:
            fprintf(stdout, "mtp-tracks: Successfully connected\n");
            fflush(stdout);
    }


    /* iterate through connected MTP devices */
    for (iter = device_list; iter != NULL; iter = iter->next) {
        this->device = iter;
        cout << "Device being synced " << LIBMTP_Get_Friendlyname(device) << "\n";
        return true;
    }

}

void YBMtpSync::synchronizeDevice(string rootPath, SyncInfo syncInfo) {

    vector<Track> toBeAdded = syncInfo.toBeAdded;
    for (int i = 0; i < toBeAdded.size(); i++) {
        addTrack(rootPath, toBeAdded[i]);
    }

    deleteTracks(syncInfo.toBeDeleted);

}

void YBMtpSync::addTrack(string rootPath, Track track) {
    // Get the directory after rootPath
    int length = rootPath.length();
    string relativePath = track.filename.substr(length);
    vector<string> tokens;
    this->tokenize(relativePath, tokens);
    int parentId = getParentId(tokens);
    sendTrack(track.filename.c_str(), parentId, track);
}

int YBMtpSync::sendTrack(char const* from_path, uint32_t parent_id, Track track) {
    char *filename;
    char num[80];
    uint64_t filesize;
#ifdef __USE_LARGEFILE64
    struct stat64 sb;
#else
    struct stat sb;
#endif
    LIBMTP_track_t *trackmeta;
    LIBMTP_album_t *albuminfo;
    int ret;

    trackmeta = LIBMTP_new_track_t();
    albuminfo = LIBMTP_new_album_t();

#ifdef __USE_LARGEFILE64
    if (stat64(from_path, &sb) == -1) {
#else
    if (stat(from_path, &sb) == -1) {
#endif
        fprintf(stderr, "%s: ", from_path);
        perror("stat");
        return 1;
    } else if (S_ISREG(sb.st_mode)) {
#ifdef __USE_LARGEFILE64
        filesize = sb.st_size;
#else
        filesize = (uint64_t) sb.st_size;
#endif
        trackmeta->filetype = find_filetype(from_path);
        if (!LIBMTP_FILETYPE_IS_TRACK(trackmeta->filetype)) {
            printf("Not a valid track codec: \"%s\"\n", LIBMTP_Get_Filetype_Description(trackmeta->filetype));
            return 1;
        }

        printf("Title:     %s\n", track.title.c_str());
        trackmeta->title = strdup(track.title.c_str());

        printf("Album:     %s\n", track.album.c_str());
        trackmeta->album = strdup(track.album.c_str());
        albuminfo->name = strdup(track.album.c_str());

        printf("Album artist:    %s\n", track.artist.c_str());
        albuminfo->artist = strdup(track.artist.c_str());
        printf("Artist:    %s\n", track.artist.c_str());
        trackmeta->artist = strdup(track.artist.c_str());


        printf("Length:    %d\n", track.length);
        // Multiply by 1000 since this is in milliseconds
        trackmeta->duration = track.length * 1000;

        // We should always have this
        if (filename != NULL) {
            trackmeta->filename = strdup(filename);
        }
        trackmeta->filesize = filesize;
        trackmeta->parent_id = parent_id;

        printf("Sending track...\n");
        ret = LIBMTP_Send_Track_From_File(device, from_path, trackmeta, NULL, NULL);
        printf("\n");
        if (ret != 0) {
            printf("Error sending track.\n");
            LIBMTP_Dump_Errorstack(device);
            LIBMTP_Clear_Errorstack(device);
        } else {
            printf("New track ID: %d\n", trackmeta->item_id);
        }

        /* Add here add to album call */
        if (track.album.c_str()) {
            ret = addTrackToAlbum(albuminfo, trackmeta);
        }

        LIBMTP_destroy_album_t(albuminfo);
        LIBMTP_destroy_track_t(trackmeta);

        return 0;
    }
    return 0;
}

int YBMtpSync::addTrackToAlbum(LIBMTP_album_t *albuminfo, LIBMTP_track_t *trackmeta) {
    LIBMTP_album_t *album;
    LIBMTP_album_t *found_album = NULL;
    int ret;

    /* Look for the album */
    album = LIBMTP_Get_Album_List(device);
    while (album != NULL) {
        if ((album->name != NULL &&
                album->artist != NULL &&
                !strcmp(album->name, albuminfo->name) &&
                !strcmp(album->artist, albuminfo->artist)) ||
                (album->name != NULL &&
                album->composer != NULL &&
                !strcmp(album->name, albuminfo->name) &&
                !strcmp(album->composer, albuminfo->composer))) {
            /* Disconnect this album for later use */
            found_album = album;
            album = album->next;
            found_album->next = NULL;
        } else {
            LIBMTP_album_t *tmp;

            tmp = album;
            album = album->next;
            LIBMTP_destroy_album_t(tmp);
        }
    }

    if (found_album != NULL) {
        uint32_t *tracks;

        tracks = (uint32_t *) malloc((found_album->no_tracks + 1) * sizeof (uint32_t));
        printf("Album \"%s\" found: updating...\n", found_album->name);
        if (!tracks) {
            printf("failed malloc in add_track_to_album()\n");
            return 1;
        }
        found_album->no_tracks++;
        if (found_album->tracks != NULL) {
            memcpy(tracks, found_album->tracks, found_album->no_tracks * sizeof (uint32_t));
            free(found_album->tracks);
        }
        tracks[found_album->no_tracks - 1] = trackmeta->item_id;
        found_album->tracks = tracks;
        ret = LIBMTP_Update_Album(device, found_album);
        LIBMTP_destroy_album_t(found_album);
    } else {
        uint32_t *trackid;

        trackid = (uint32_t *) malloc(sizeof (uint32_t));
        *trackid = trackmeta->item_id;
        albuminfo->tracks = trackid;
        albuminfo->no_tracks = 1;
        albuminfo->storage_id = trackmeta->storage_id;
        printf("Album doesn't exist: creating...\n");
        ret = LIBMTP_Create_New_Album(device, albuminfo);
        /* albuminfo will be destroyed later by caller */
    }

    if (ret != 0) {
        printf("Error creating or updating album.\n");
        printf("(This could be due to that your device does not support albums.)\n");
        LIBMTP_Dump_Errorstack(device);
        LIBMTP_Clear_Errorstack(device);
    } else {
        printf("success!\n");
    }
    return ret;
}

int YBMtpSync::getParentId(vector<string> tokens) {

    LIBMTP_folder_t* parent = getOrCreateFolder("Music", NULL);

    // We iterate to tokens.size() - 1 because the last token is the file name.
    if (tokens.size() > 1) {
        for (int i = 0; i < tokens.size() - 1; i++) {
            cout << "Getting or Creating folder " << tokens[i] << " under " << parent->name << "[" << parent->folder_id << "]\n";
            parent = getOrCreateFolder(tokens[i], parent);
        }
    }

    int id = parent->folder_id;
    return id;

}

LIBMTP_folder_t* YBMtpSync::getOrCreateFolder(string folderName, LIBMTP_folder_t *folder) {

    LIBMTP_folder_t* child = NULL;
    if (folder == NULL) {
        child = LIBMTP_Get_Folder_List(device);
    } else {
        child = folder->child;
    }

    // for each child
    // if the child name is folderName, return child
    while (child != NULL) {

        if (folderName.compare(child->name) == 0) {
            return child;
        }
        child = child->sibling;
    }

    char * folderNameC;
    folderNameC = new char[folderName.length() + 1];
    strcpy(folderNameC, folderName.c_str());

    // create a new folder with folderName
    // return it.
    int newId = LIBMTP_Create_Folder(device, folderNameC, folder->folder_id, 0);
    if (newId == 0) {
        cout << "Folder creation failed";
        return NULL;
    } else {
        // folder now needs to be refreshed and pointed to one which has the new child folder.
        LIBMTP_folder_t* root = LIBMTP_Get_Folder_List(device);
        LIBMTP_folder_t* newReferenceToParent = getFolderById(root, folder->folder_id);
        return getOrCreateFolder(folderName, newReferenceToParent);
    }
}

LIBMTP_folder_t* YBMtpSync::getFolderById(LIBMTP_folder_t* folder, int id) {

    if (folder == NULL) {
        return NULL;
    }

    if (folder->folder_id == id) {
        return folder;
    } else {
        LIBMTP_folder_t* searchResultInChild = getFolderById(folder->child, id);
        if (searchResultInChild != NULL) {
            return searchResultInChild;
        } else {
            return getFolderById(folder->sibling, id);
        }

    }

    return NULL;

}

void YBMtpSync::tokenize(const string& str, vector<string>& tokens, const string& delimiters) {
    // Skip delimiters at beginning.
    string::size_type lastPos = str.find_first_not_of(delimiters, 0);
    // Find first "non-delimiter".
    string::size_type pos = str.find_first_of(delimiters, lastPos);

    while (string::npos != pos || string::npos != lastPos) {
        // Found a token, add it to the vector.
        tokens.push_back(str.substr(lastPos, pos - lastPos));
        // Skip delimiters.  Note the "not_of"
        lastPos = str.find_first_not_of(delimiters, pos);
        // Find next "non-delimiter"
        pos = str.find_first_of(delimiters, lastPos);
    }
}

void YBMtpSync::disconnect() {
    LIBMTP_Release_Device_List(device);
}

SyncInfo YBMtpSync::getSynchronizeDeviceInfo(vector<string> files) {

    vector<Track> localTracks;
    vector<Track>::iterator iterator;

    iterator = localTracks.begin();
    for (int i = 0; i < files.size(); i++) {
        Track t = this->extractTrackInfo(files[i]);
        localTracks.push_back(t);
    }


    vector<Track> mtpTracks = this->mtpTracks();

    vector<Track> duplicateTracks = this->duplicateTracks(mtpTracks);

    if (duplicateTracks.size() > 0) {
        cout << "Duplicate tracks found - " << duplicateTracks.size() << "\n";
        cout << "Duplicate tracks START.\n";
        coutTracks(duplicateTracks);
        cout << "Duplicate tracks END.\n";

        cout << "Deleting duplicate tracks\n";
        this->deleteTracks(duplicateTracks);
    }

    vector<Track> tracksToDelete = this->getDifference(mtpTracks, localTracks);
    if (tracksToDelete.size() > 0) {
        cout << "These tracks will be deleted\n";
        coutTracks(tracksToDelete);
        cout << "Tracks to delete done.\n";
    }
    else {
        cout << "Nothing to delete\n";
    }

    vector<Track> tracksToAdd = this->getDifference(localTracks, mtpTracks);
    if (tracksToAdd.size() > 0) {
        cout << "These tracks will be added\n";
        coutTracks(tracksToAdd);
        cout << "Tracks to add done.\n";
    }
    else {
        cout << "Nothing to add\n";
    }

    SyncInfo syncInfo;
    syncInfo.toBeAdded = tracksToAdd;
    syncInfo.toBeDeleted = tracksToDelete;
    return syncInfo;
}

void YBMtpSync::deleteTracks(vector<Track> tracks) {
    for (int i = 0; i < tracks.size(); i++) {
        int ret = 0;
        Track trackToDelete = tracks[i];
        int id = trackToDelete.mtpTrackId;
        if (id > 0) {
            printf("Deleting %s\n", trackToDelete.filename.c_str());
            ret = LIBMTP_Delete_Object(device, id);
        }
        if (ret != 0) {
            printf("Failed to delete file:%s\n", trackToDelete.filename.c_str());
            LIBMTP_Dump_Errorstack(device);
            LIBMTP_Clear_Errorstack(device);
            ret = 1;
        }

    }

}

vector<Track> YBMtpSync::getDifference(vector<Track> source, vector<Track> destination) {
    vector<Track> difference;
    for (int i = 0; i < source.size(); i++) {
        Track sourceTrack = source[i];
        bool found = false;
        for (int j = 0; j < destination.size(); j++) {
            Track destinationTrack = destination[j];
            if (compareTracks(sourceTrack, destinationTrack)) {
                found = true;
                break;
            }
        }
        if (!found) {
            difference.push_back(sourceTrack);
        }
    }
    return difference;
}

Track YBMtpSync::extractTrackInfo(string filename) {

    TagLib::FileRef f(filename.c_str());

    Track t;
    if (!f.isNull() && f.tag()) {
        TagLib::String title = f.tag()->title();
        TagLib::String artist = f.tag()->artist();
        TagLib::String album = f.tag()->album();

        if (&title != NULL && title.length() > 0) {
            t.title = title.toCString(true);
        } else {
            char * basefile;
            basefile = new char[filename.length() + 1];
            strcpy(basefile, filename.c_str());
            t.title = basename(basefile);
        }

        if (&artist != NULL && artist.length() > 0) {
            t.artist = artist.toCString(true);
        } else {
            t.artist = "Unknown Artist";
        }

        if (&album != NULL && album.length() > 0) {
            t.album = album.toCString(true);
        } else {
            t.album = "Unkown Album";
        }
    } else {
        t.title = "Unkown Title";
        t.artist = "Unkown Artist";
        t.album = "Unkown Album";
    }

    if (!f.isNull() && f.audioProperties()) {
        int length = f.audioProperties()->length();
        t.length = length;
    } else {
        t.length = 0;
    }

    t.filename = filename;
    t.mtpTrackId = 0;

    return t;
}

void YBMtpSync::coutTracks(vector<Track> tracks) {
    for (int i = 0; i < tracks.size(); i++) {
        Track t = tracks[i];
        this->coutTrack(t);
    }
}

void YBMtpSync::coutTrack(Track t) {
    cout << "Track - " << t.title << "\n\t" << t.artist << "\n\t" << t.album <<
            "\n\t" << t.length << "\n\t" << t.filename << "\n\t" << t.mtpTrackId
            << "\n";
}

vector<Track> YBMtpSync::mtpTracks() {
    vector<Track> mtpTracks;

    if (device == NULL) {
        printf("Device is NULL\n");
        return mtpTracks;
    }

    LIBMTP_track_t *tracks;
    tracks = LIBMTP_Get_Tracklisting_With_Callback(device, NULL, NULL);
    if (tracks == NULL) {
        cout << "No tracks in " << LIBMTP_Get_Friendlyname(device) << "\n";
    } else {
        LIBMTP_track_t *track, *tmp;
        track = tracks;

        while (track != NULL) {
            tmp = track;

            Track t;

            if (track->title != NULL) {
                t.title = track->title;
            } else {
                t.title = "Unknown Title";
            }

            if (track->artist != NULL) {
                t.artist = track->artist;
            } else {
                t.artist = "Unknown Artist";
            }

            if (track->album != NULL) {
                t.album = track->album;
            } else {
                t.album = "Unknown Album";
            }

            t.length = track->duration / 1000;
            t.mtpTrackId = track->item_id;

            if (track->filename != NULL) {
                t.filename = track->filename;
            } else {
                t.filename = "Unknown File name";
            }

            cout << "Read track from MTP device\n";
            coutTrack(t);
            mtpTracks.push_back(t);

            track = track->next;
            LIBMTP_destroy_track_t(tmp);
        }
    }

    return mtpTracks;

}

vector<Track> YBMtpSync::duplicateTracks(vector<Track> tracks) {

    vector<Track> duplicates;

    for (int i = 0; i < tracks.size(); i++) {
        bool duplicateFound = false;
        Track iTrack = tracks[i];
        for (int j = i + 1; j < tracks.size(); j++) {
            Track jTrack = tracks[j];

            if (this->compareTracks(iTrack, jTrack)) {
                duplicates.push_back(jTrack);
                duplicateFound = true;
                break;
            }

        }
        if (!duplicateFound) {
            cout << "No duplicate found for - \n";
            coutTrack(iTrack);
        }
    }

    return duplicates;
}

bool YBMtpSync::compareTracks(Track trackOne, Track trackTwo) {
    if (trackOne.title.compare(trackTwo.title) == 0 &&
            trackOne.artist.compare(trackTwo.artist) == 0 &&
            trackOne.album.compare(trackTwo.album) == 0 &&
            trackOne.length == trackTwo.length) {
        return true;
    } else {
        return false;
    }
}
